package edu.neumont.pro180._2009_q2;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Scanner;

import edu.neumont.pro180._2009_q2.interfaces.ICommand;
import edu.neumont.pro180._2009_q2.interfaces.IGamePiece;
import edu.neumont.pro180._2009_q2.interfaces.IGamePieceFactory;

/**
 * Runs a game of checkers with an instance of Board. It uses the console for
 * input and output.
 * 
 * @author Tyler Young
 * 
 */
public class BoardGame {
	protected int columnCount, rowCount, checkerCount;
	protected Board board;
	// private Checker selectedPiece;
	protected Square currentSquare;
	@SuppressWarnings("unchecked")
	protected Map<Class, String> unselectedPieceViews = new HashMap<Class, String>();
	@SuppressWarnings("unchecked")
	protected Map<Class, String> selectedPieceViews = new HashMap<Class, String>();

	public BoardGame() {
		unselectedPieceViews.put(Checker.class, "o");
		selectedPieceViews.put(Checker.class, "O");
	}

	// Characters used for user input.
	/**
	 * The character used to move the cursor up.
	 */
	public static final char up = 's';
	/**
	 * The character used to move the cursor left.
	 */
	public static final char left = 'a';
	/**
	 * The character used to move the cursor down.
	 */
	public static final char down = 'x';
	/**
	 * The character used to move the cursor right.
	 */
	public static final char right = 'd';
	/**
	 * The character used to exit the game.
	 */
	public static final char exit = 'q';
	/**
	 * The character used to select a piece.
	 */
	public static final char select = 't';

	// Characters used to display the board state to the user.
	/**
	 * The character used for a Square that is unoccupied.
	 */
	public static final String unoccupied = ".";
	/**
	 * The character used for a Square that is unoccupied and is a valid move
	 * for the currently selected game piece.
	 */
	public static final String unoccupied_isMove = "-";
	/**
	 * The character used for a Square that is is unoccupied and is currently
	 * selected.
	 */
	public static final String unoccupied_isSelected = "^";
	/**
	 * The character used for a Square that is unoccupied and is a valid move
	 * for the currently selected game piece, and is currently selected by the
	 * cursor.
	 */
	public static final String unoccupied_isMove_isSelected = "+";
	/**
	 * The character used for a Square that is occupied and is a valid move for
	 * the currently selected game piece.
	 */
	public static final String occupied = "o", occupied_isMove = "#";
	/**
	 * The character used for a Square that is is occupied and is currently
	 * selected.
	 */
	public static final String occupied_isSelected = "O";
	/**
	 * The character used for a Square that is occupied and is a valid move for
	 * the currently selected game piece, and is currently selected by the
	 * cursor.
	 */
	public static final String occupied_isMove_isSelected = "*";

	/**
	 * The string used to prepend each row of the board when it's displayed at
	 * the console. Usually just whitespace.
	 */
	private static final String boardPrefix = "\t\t";
	private static final String selectedTeamPrefix = "(";
	private static final String vulnerablePiecePrefix = "-";
	private static final String selectedKillPiecePrefix = "*";
	private static final String defaultPrefix = " ";

	/**
	 * Checks to ensure that everything is set up properly before the game can
	 * commence.
	 * 
	 * @return true if the current settings on the Board are valid, false if
	 *         they are not.
	 */
	public boolean validateSettings() {
		return (columnCount > 1) && (rowCount > 1)
				&& (checkerCount < ((columnCount * rowCount) / 2) - 1);
	}

	/**
	 * Plays the game. Assumes all settings for the game are valid.
	 * 
	 * @throws IllegalStateException
	 *             If the rows/columns/checker count weren't previously set up
	 *             correctly
	 */
	public void playGame() throws IllegalStateException {
		try {
			this.board = new Board(this.rowCount, this.columnCount);
		} catch (IllegalArgumentException e) {
			throw new IllegalStateException(
					"Rows/Columns weren't set up correctly, cannot play game",
					e);
		}

		try {
			randomlyPopulateBoard(this.board, this.checkerCount);
		} catch (IllegalArgumentException e) {
			throw new IllegalStateException(
					"Checker count wasn't set up correctly, cannot play game",
					e);
		}

		this.currentSquare = this.board.getSquare(new Point(0, 0));
		Scanner scanner = new Scanner(System.in);
		do {
			printKey();
			printBoard(this.board);
			printInstructions();
		} while (processUserInput(scanner.nextLine()));
	}

	/**
	 * Randomly populates the board with the number of checker pieces specified.
	 * If there are too many checker pieces, it will ignore the command to
	 * populate the board.
	 * 
	 * @param board
	 *            The Board which will be populated with game pieces.
	 * @param totalPieces
	 *            The total number of pieces to place on the board. Must be less
	 *            than (# of squares / 2) - 1
	 */
	protected void randomlyPopulateBoard(Board board, int totalPieces) {
		if (totalPieces >= ((board.getColumnCount() * board.getRowCount()) / 2) - 1
				|| totalPieces < 1) {
			throw new IllegalArgumentException(
					"Pieces to place on the board must be between 1, and 1 less than half the number of squares");
		}

		ArrayList<Square> validSquares = new ArrayList<Square>();
		// Make collection of all the valid locations
		for (int row = 0; row < board.getRowCount(); ++row) {
			for (int col = 0; col < board.getColumnCount(); ++col) {
				if (row % 2 == 0) {
					if (col % 2 == 0) {
						validSquares.add(board.getSquare(new Point(col, row)));
					}
				} else {
					if (col % 2 == 1) {
						validSquares.add(board.getSquare(new Point(col, row)));
					}
				}
			}
		}

		Random random = new Random();
		int placedPieces = 0;
		while (placedPieces < totalPieces) {
			Point p = new Point(random.nextInt(board.getColumnCount()), random
					.nextInt(board.getRowCount()));
			Square s = board.getSquare(p);
			if (validSquares.contains(s) && !s.hasGamePiece()) {
				Checker checker = new Checker();
				checker.setPosition(s);
				++placedPieces;
			}
		}
	}

	protected void createPiecesOnSquares(Point[] points, Board board,
			Player player, IGamePieceFactory pieceFactory) {
		for (Point point : points) {
			Square square = board.getSquare(point);
			if (null != square) {
				pieceFactory.CreatePiece(player, square);
			}
		}
	}

	/**
	 * Move the cursor up, down, left, or right, or select game pieces or an
	 * available move.
	 * 
	 * @param command
	 *            The command string to execute.
	 * @return true if a valid command was entered, false if no command was
	 *         executed.
	 */
	public boolean processUserInput(String command) {
		if (command.length() > 0) {
			switch (command.charAt(0)) {
			case up:
				currentSquare = currentSquare
						.getNeighborOrSelf(new Point(0, 1));
				break;
			case down:
				currentSquare = currentSquare
						.getNeighborOrSelf(new Point(0, -1));
				break;
			case left:
				currentSquare = currentSquare
						.getNeighborOrSelf(new Point(-1, 0));
				break;
			case right:
				currentSquare = currentSquare
						.getNeighborOrSelf(new Point(1, 0));
				break;
			case select:
				makeSelection();
				break;
			case exit:
				return false;
			}
		}
		return true;
	}

	/**
	 * Select the square that the cursor is currently on. If no piece is
	 * currently selected, it will select the checker under the cursor. If a
	 * piece is selected, it will make the move that's available for the current
	 * piece.
	 */
	protected void makeSelection() {
		Player player = board.getActivePlayer();
		ICommand nextMove = player.getNextMove();
		if (nextMove != null) {
			board.doMove(nextMove);
			board.nextPlayer();
		} else {
			if (currentSquare.hasGamePiece()
					&& player.getActivePieces().contains(currentSquare.getGamePiece())) {
				player.setSelectedPiece(currentSquare.getGamePiece());
			} else {
				player.setPieceDestination(currentSquare);
			}
		}
		// if (selectedPiece != null) {
		// if (selectedPiece.getPossibleMoves().contains(currentSquare)) {
		// selectedPiece.setPosition(currentSquare);
		//				
		// board.nextPlayer();
		// }
		// selectedPiece = null;
		// } else if (currentSquare.hasGamePiece()) {
		// // Ensure the piece being selected belongs to the current player
		// if (board.getActivePlayer().getActivePieces().contains(
		// currentSquare.getGamePiece())) {
		// selectedPiece = currentSquare.getGamePiece();
		// }
		// }
	}

	/**
	 * Prints the board to the console.
	 * 
	 * @param board
	 *            The Board to print to the console.
	 */
	protected void printBoard(Board board) {
		StringBuilder buffer = new StringBuilder();
		Collection<Square> possibleMoves = null;
		IGamePiece selectedPiece;
		if (null != (selectedPiece = board.getActivePlayer().getSelectedPiece())) {
			possibleMoves = selectedPiece.getPossibleSquares();
		}
		// Print the column headers
		for (int row = rowCount - 1; row >= 0; --row) {
			buffer.append(boardPrefix);
			for (int column = 0; column < columnCount; ++column) {
				Square square = board.getSquare(new Point(column, row));
				boolean isOccupied = square.hasGamePiece();
				boolean isSelected = square == currentSquare;
				boolean isMove = (possibleMoves != null)
						&& possibleMoves.contains(square);
				String squareText = "";

				if (isOccupied) {
					String prefix;
					if (board.getActivePlayer() == square.getGamePiece()
							.getPlayer()) {
						prefix = selectedTeamPrefix;
					} else if (isMove) {
						if (isSelected) {
							prefix = selectedKillPiecePrefix;
						} else {
							prefix = vulnerablePiecePrefix;
						}
					} else {
						prefix = defaultPrefix;
					}

					if (isSelected) {
						squareText = prefix
								+ selectedPieceViews.get(square.getGamePiece()
										.getClass());
					} else {
						squareText = prefix
								+ unselectedPieceViews.get(square
										.getGamePiece().getClass());
					}
				} else {
					if (isMove) {
						if (isSelected) {
							squareText = defaultPrefix
									+ unoccupied_isMove_isSelected;
						} else {
							squareText = defaultPrefix + unoccupied_isMove;
						}
					} else if (isSelected) {
						squareText = defaultPrefix + unoccupied_isSelected;
					} else {
						squareText = defaultPrefix + unoccupied;
					}
				}

				buffer.append(squareText);
			}
			buffer.append("\n");
		}
		System.out.print(buffer.toString());
	}

	/**
	 * Prints the "key" to the board. Shows the user how to interpret the pieces
	 * on the board.
	 */
	protected void printKey() {
		System.out.print("Key:" + "\t\t\tNormal\tSelected\n" + "\tEmpty:\t\t  "
				+ BoardGame.unoccupied + "\t  "
				+ BoardGame.unoccupied_isSelected + "\n" + "\tOccupied:\t  "
				+ BoardGame.occupied + "\t  " + BoardGame.occupied_isSelected
				+ "\n" + "\tOpen Move:\t  " + BoardGame.unoccupied_isMove
				+ "\t  " + BoardGame.unoccupied_isMove_isSelected + "\n"
				+ "\tOccupied move:\t  " + BoardGame.occupied_isMove + "\t  "
				+ BoardGame.occupied_isMove_isSelected + "\n" + "\n");
	}

	/**
	 * Prints the instructions for the user to the console.
	 */
	protected void printInstructions() {
		System.out.print("Choose from the following commands:" + "\n\tLeft:\t"
				+ BoardGame.left + "\tRight:\t" + BoardGame.right + "\n\tUp:\t"
				+ BoardGame.up + "\tDown:\t" + BoardGame.down + "\n\tSelect:\t"
				+ BoardGame.select + "\tExit:\t" + BoardGame.exit + "\n");
	}
}
